package br.com.velp.sudecap.hibernate.intercept;

import br.com.velp.sudecap.entidade.EntityBase;
import br.com.velp.sudecap.enumerator.TipoOperacaoEnum;
import br.com.velp.sudecap.mb.contexto.ContextoBean;
import br.com.velp.sudecap.util.JsfUtil;
import br.com.velp.sudecap.util.MensagemUtil;
import java.io.Serializable;
import java.lang.reflect.Field;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;
import javax.persistence.Column;
import javax.persistence.JoinColumn;
import javax.sql.DataSource;
import org.hibernate.Session;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

/**
 * <sumario>
 * <objetivo> "Rastreabilidade" </objetivo>
 * <autor> EDUARDO SANTOS DE OLIVEIRA </autor>
 * <versao>1.0.0.0</versao>
 * <data_criacao> 31.03.2013 </data_criacao>
 * <data_ultima_alteracao> 18.04.2013 </data_ultima_alteracao>
 * <descricao> Classe Responsável Por Realizar o INSERT Em Tabelas de LOG de Auditoria 
 *             do Sistema. Cada Tabela de Auditoria Deve Conter as Colunas 
 *             OPERACAO | DATA_HORA_OPERACAO | USUARIO_OPERACAO. Caso Seja Necessário
 *             Desconsiderar a Persistência de Algum Atributo de Uma Entidade, Deve-se
 *             Annotá-lo Com @Unauditable.
 * </descricao>
 * </sumario>
 */
@Component
public class RegistraLogAuditoria implements Serializable {
    
    public static final Long serialVersionUID = 1l;
    
    //Constantes Para Geração da Query de INSERT dos Registros de Log
    private static final String PARENTESES_ESQUERDO = " ( ";
    private static final String PARENTESES_DIREITO = " ) ";
    private static final String CLAUSULA_INSERT = " INSERT INTO ";
    private static final String VALUES = " VALUES ";
    private static final String TOKEN_VIRGULA = ", ";
    private static final String TOKEN_INTERROGACAO = "?";
    private static final String COLUNA_TIPO_OPERACAO = "OPERACAO";
    private static final String COLUNA_DATA_HORA_OPERACAO = "DATA_HORA_OPERACAO";
    private static final String COLUNA_USUARIO_OPERACAO = "USUARIO_OPERACAO";
    private static final String CONTEXTO_BEAN = "contextoBean";
    
    //Variável Responsável Por Armazenar Qual o Tipo de Operação Está 
    //Gerando o Registro de LOG (INSERT | UPDATE | DELETE)
    private TipoOperacaoEnum tipoOperacao;
    
    @Autowired
    private DataSource dataSource;

    public void registrarLog(Object entity, Session session, TipoOperacaoEnum operacao){
        
        //Armazenando o Tipo de Operação A Ser Registrada (INSERT | UPDATE | DELETE)
        this.tipoOperacao = operacao;
        
        //Capturando a Annotation @Auditable
        Auditable auditable = entity.getClass().getAnnotation(Auditable.class);
        
        //Caso o Objeto Não Possua a Annotation @Auditable, a Operação de
        //Log Será Desconsiderada
        if(auditable == null){
            return;
        }
        
        //Variáveis Necessárias Para Geração da Cláusula SQL INSERT
        List<Object> values = new LinkedList<Object>();
        StringBuilder sqlColumNames = new StringBuilder();
        StringBuilder sqlParams = new StringBuilder();
        StringBuilder sqlInsert = new StringBuilder();

        //Adicionandos os Valores Padrões a Serem Utilizados Pelos Registros
        //de Auditoria do Sistema - Colunas:
        // * OPERACAO (Insert | Update | Delete)
        // * DATA_HORA_OPERACAO (java.sql.Date)
        // * USUARIO_OPERACAO (Login do Usuário Que Realizou a Operacao)
        adicionarValoresPadroesAuditoria(sqlColumNames, sqlParams, values);
        
        //Declarando a Conexão SQL e o PreparedStatement
        Connection conn = null;
        PreparedStatement stmt = null;
        
        try {

            //Iterando os Atributos do Objeto, Para Capturar Os Nomes
            //das Colunas e Seus Respectivos Valores
            for (Field field : entity.getClass().getDeclaredFields()) {
                adicionarValorPersistente(field, sqlColumNames, sqlParams, values, entity);
            }
            
            //Montando a Cláusula SQL Responsável Por Efetuar o Insert Na Tablea de LOG
            sqlInsert.append(CLAUSULA_INSERT);
            sqlInsert.append(auditable.logTableName());
            sqlInsert.append(PARENTESES_ESQUERDO);
            sqlInsert.append(sqlColumNames.toString());
            sqlInsert.append(PARENTESES_DIREITO);
            sqlInsert.append(VALUES);
            sqlInsert.append(PARENTESES_ESQUERDO);
            sqlInsert.append(sqlParams);
            sqlInsert.append(PARENTESES_DIREITO);
            
            //Criando Statement e Executando a Cláusula INSERT 
            conn = this.dataSource.getConnection();
            stmt = conn.prepareStatement(sqlInsert.toString());
            
            for(int i = 0; i < values.size(); i++){
                stmt.setObject(i + 1, values.get(i));
            }
            
            stmt.execute();

        } catch (Exception e) {

            e.printStackTrace();

        } finally{
            
            try {
                
                if(stmt != null){
                    stmt.close();
                }
                
                if(conn != null){
                    conn.close();
                }
                
            } catch (Exception e) {
                
                MensagemUtil.exibirMensagemErro(e.getMessage());
                
            }
            
        }       
        
    }
    
    /**
     * 
     * Utiliza Reflection para Recuperar o Valor do Campo (Field) e 
     * Com Base Nas Annotations do Hibernate Captura os Nomes das Colunas
     * Para Geração da Cláusula INSERT
     * 
     * @param Field field
     * @param StringBuilder sqlColumNames
     * @param StringBuilder sqlParams
     * @param Map<String, Object> values
     * @param Object entity
     * @throws Exception
     * 
     */
    private void adicionarValorPersistente(Field field, StringBuilder sqlColumNames,
            StringBuilder sqlParams, List<Object> values, Object entity) throws Exception {
        
        //Capturando a Annotation @Unversioned Presente no Campo
        Unauditable unversioned = field.getAnnotation(Unauditable.class);
        
        //Caso o Field Tenha Sido Anotado Com @Unversioned, Este Será Desconsiderado
        //Ao Efetuar o Registro de LOG
        if(unversioned != null){
            return;
        }

        //Recuperando o Valor do FIELD(Atributo) do Objeto
        field.setAccessible(true);
        Object value = field.get(entity);

        //Desconsiderando Valor Nulo ou Vazio Para Execução da Cláusula INSERT
        if (value == null || "".equals(value)) {
            return;
        }

        //Caso o Valor Seja Instância de EntityBase (Indica Relacionamento ManyToOne)
        //Deve-se Recuperar o Valor da Chave(ID) da Entidade
        if (value instanceof EntityBase) {
            Field fieldId = value.getClass().getDeclaredField("id");
            fieldId.setAccessible(true);
            value = fieldId.get(value);
        }

        //Obtendo o Nome da Coluna Para Realizar o Insert na Tabela de LOG
        Column column = field.getAnnotation(Column.class);
        JoinColumn joinColumn = field.getAnnotation(JoinColumn.class);

        //Configurando o Nome da Coluna Para Realização do Insert,
        //O Nome do Parâmetro Para a Coluna e Populando Mapa
        //Com Nome do Parâmetro + Seu Respectivo Valor
        if (column != null) {
            adicionarColuna(sqlColumNames, column.name());
            adicionarParametro(sqlParams);
            adicionarValor(values, value);
        } else if (joinColumn != null) {
            adicionarColuna(sqlColumNames, joinColumn.name());
            adicionarParametro(sqlParams);
            adicionarValor(values, value);
        }

    }

    /**
     * 
     * Adicionando Nomes das Colunas no StringBuilder, Para Realização do Insert
     *
     * @param StringBuilder input
     * @param String nomeColuna
     *
     */
    private void adicionarColuna(StringBuilder sqlColumnNames, String nomeColuna) {
        if (!sqlColumnNames.toString().isEmpty()) {
            sqlColumnNames.append(TOKEN_VIRGULA);
        }
        sqlColumnNames.append(nomeColuna);
    }

    /**
     * 
     * Adicionando Nomes dos Parâmetros no StringBuilder, Para Realização do Insert
     *
     * @param StringBuilder sqlParams
     *
     */
    private void adicionarParametro(StringBuilder sqlParams) {
        if (!sqlParams.toString().isEmpty()) {
            sqlParams.append(TOKEN_VIRGULA);
        }
        sqlParams.append(TOKEN_INTERROGACAO);
    }

    /**
     * 
     * Adiciona na Lista de Valores o Valor Capturado do Field, Convertendo Para java.sql.Date Quando o
     * Objeto For do Tipo java.util.Date (Necessário Quando Se Executa NativeQueries).
     * Realiza Também a Conversão dos Tipos Booleanos Para Integer(1) ou Integer(0)
     *
     * @param Map<String, Object> values
     * @param String nomeParametro
     * @param String valorParametro
     *
     */
    private void adicionarValor(List<Object> values, Object valorParametro) {
        if(valorParametro == null){
            values.add(valorParametro);
        } if (valorParametro instanceof Date) {
            Date valor = (Date) valorParametro;
            values.add(new java.sql.Date(valor.getTime()));
        } else if (valorParametro instanceof Boolean){
            Boolean value = (Boolean) valorParametro;
            values.add(value ? new Integer(1) : new Integer(0));
        } else {
            values.add(valorParametro);
        }
    }
    
    /**
     * 
     * Método Responsável Por Adicionar as Colunas e Valores Padrão 
     * Existentes No Sistema de LOG. Ou Seja, Todas as Tabelas de LOG Devem Conter
     * as Colunas Padrão (OPERACAO, DATA_HORA_OPERACAO, USUARIO_OPERACAO)
     * 
     * @param StringBuilder sqlColumNames
     * @param StringBuilder sqlParams
     * @param Map<String, Object> params 
     * 
     */
    private void adicionarValoresPadroesAuditoria(StringBuilder sqlColumNames, StringBuilder sqlParams, List<Object> valores){
        sqlColumNames.append(getColunasPadraoAuditoria());
        sqlParams.append(getParametrosPadraoAuditoria());
        valores.addAll(getValoresColunasPadrao());
    }
    
    /**
     * 
     * Método Responsável Por Retornar Uma String Contendo as
     * Colunas Padrão Que Devem Existir No LOG de Registros
     * do Sistema (OPERACAO, DATA_HORA_OPERACAO, USUARIO_OPERACAO)
     * 
     * @return String colunasPadraoAuditoria
     * 
     */
    private String getColunasPadraoAuditoria(){
        StringBuilder colunasPadrao = new StringBuilder();
        colunasPadrao.append(COLUNA_TIPO_OPERACAO);
        colunasPadrao.append(TOKEN_VIRGULA);
        colunasPadrao.append(COLUNA_DATA_HORA_OPERACAO);
        colunasPadrao.append(TOKEN_VIRGULA);
        colunasPadrao.append(COLUNA_USUARIO_OPERACAO);
        return colunasPadrao.toString();
    }
    
    /**
     * 
     * Método Responsável Por Retornar Uma String Contendo os
     * Parâmetros Padrão a Serem Adicionados na Cláusula SQL,
     * Com Base nas Colunas Padrão do Sistema de LOG
     * (OPERACAO, DATA_HORA_OPERACAO, USUARIO_OPERACAO)
     * 
     * @return String parametrosPadraoAuditoria
     * 
     */
    private String getParametrosPadraoAuditoria(){
        StringBuilder parametrosPadrao = new StringBuilder();
        parametrosPadrao.append(TOKEN_INTERROGACAO);
        parametrosPadrao.append(TOKEN_VIRGULA);
        parametrosPadrao.append(TOKEN_INTERROGACAO);
        parametrosPadrao.append(TOKEN_VIRGULA);
        parametrosPadrao.append(TOKEN_INTERROGACAO);
        return parametrosPadrao.toString();
    }
    
    /**
     * 
     * Método Responsável Por Gerar Uma Lista
     * Contendo os Valores Padrões Para Registro dos LOGS
     * do Sistema (OPERACAO | DATA_HORA_OPERACAO | USUARIO_OPERACAO)
     * 
     * @return List<Object> valoresPadrao
     * 
     */
    private List<Object> getValoresColunasPadrao(){
        ContextoBean contexto = (ContextoBean) JsfUtil.recuperarObjetoSessao(CONTEXTO_BEAN);
        List<Object> valores = new LinkedList<Object>();
        valores.add(this.tipoOperacao.getValue());
        valores.add(new java.sql.Timestamp(new Date().getTime()));
        valores.add(contexto != null ? contexto.getUsuario().getLogin() : "IMPORTACAO");
        return valores;
    } 
    
}
